Skip to content

runtime/wasm: add opt-in non-moving collector (based on #2197) - #2203

Draft
cpunion wants to merge 30 commits into
xgo-dev:mainfrom
cpunion:codex/tinygogc-wasm
Draft

runtime/wasm: add opt-in non-moving collector (based on #2197)#2203
cpunion wants to merge 30 commits into
xgo-dev:mainfrom
cpunion:codex/tinygogc-wasm

Conversation

@cpunion

@cpunion cpunion commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Depends on #2197.

Part of #2152 (stage T).

Problem

Wasm currently selects nogc because BDWGC is unavailable. That leaves J32, J64, and P1 without a runtime-owned allocator, reclamation, memory statistics, or linear-memory growth policy.

Enabling collection by default is not yet correct: LLGo may keep a live Go pointer only in an SSA/wasm local across runtime.GC, and a linear-stack scan cannot observe that value. Suspended-G root publication is also intentionally deferred to stage D. This PR therefore provides the collector behind the temporary internal llgo_wasm_gc build tag while preserving the existing default nogc behavior.

Implementation

  • Refactor the existing bare-metal block allocator and conservative mark/sweep core behind narrow memory-layout, growth, root-scan, and stack-stat hooks.
  • Reuse the same runtime integration for bare-metal and wasm instead of duplicating allocation, defer-node, and runtime.ReadMemStats shims.
  • Add J32/J64/P1 adapters based on __global_base, __data_end, __heap_base, the active linear stack boundary, and WebAssembly page growth.
  • Relocate collector metadata when linear memory grows and retain at least one MiB of growth per step.
  • Route Emscripten standard and emscripten_builtin_* allocation entry points to the collector. -sMALLOC=none is injected only when llgo_wasm_gc is selected, so default wasm builds retain their current allocator.
  • Keep finalizers and cleanup hooks unsupported in this initial collector.
  • Reject llgo_wasm_gc with threaded P1 builds; the collector is intentionally single-worker and requires LLGO_WASI_THREADS=0 for P1.

The collector is single-worker. It scans linear-memory globals and the active linear stack, but does not claim that wasm locals or suspended Asyncify contexts are roots. Stage D will add compiler-maintained root records and then remove the opt-in gate.

Validation

The fixture covers:

  • runtime.ReadMemStats allocation and reclamation counters;
  • global-root retention and recursive retention through a global slice;
  • reclamation of 1,024 unreachable objects;
  • 64 KiB aligned Emscripten allocation and data integrity;
  • bounded linear-memory growth past each target initial heap, followed by collection and live-data verification.
Target Result
J32: llgo build -target wasm -tags=llgo_wasm_gc built and executed with Node: wasm gc ok
J64: GOOS=js GOARCH=wasm ... -tags=llgo_wasm_gc built and executed with Node Memory64: wasm gc ok
P1: GOOS=wasip1 GOARCH=wasm LLGO_WASI_THREADS=0 built, validated with wasm-tools, and executed with Wasmtime 39 and exceptions enabled: wasm gc ok

Local bounded checks used GOMAXPROCS=2 and -p=1. The full macOS internal/build and internal/crosscompile tests passed with 73.9% and 80.9% package coverage; both newly added build helpers report 100% coverage. The runtime module passed on macOS and in an Ubuntu 24.04 container limited to 15 GiB and 2 CPUs.

The updated #2197-based stack passes the full GitHub Ubuntu/macOS matrix and Codecov; local macOS target and coverage checks also pass.

Native and embedded cost

Compared with the direct #2197 base using identical inputs:

  • native Mach-O section sizes are identical in every section, total 67,945 bytes; only the linker UUID differs;
  • cortex-m-qemu embedded ELFs are byte-for-byte identical (same SHA-256), with .text 92 bytes and total sections 4,353 bytes.

Wasm-only sources and the allocator linker option are excluded unless the internal tag is selected, so this PR adds no default native, embedded, or wasm runtime path.

Opt-in wasm size

Using the same minimal wasm-runtime fixture for both modes:

Target default nogc opt-in GC Delta
J32 wasm payload 123,632 B 127,712 B +4,080 B (+3.3%)
J64 wasm payload 134,372 B 149,948 B +15,576 B (+11.6%)

These bytes are paid only by explicitly tagged builds. Runtime throughput and pause measurements remain follow-up data once compiler-maintained roots make collection semantically usable for general programs.

Review scope

Excluding #2197, this PR changes 29 files with 546 additions and 62 deletions. The first commit is a behavior-preserving platform-hook refactor; the second contains the wasm facility; later commits add CI, make the growth test target-relative, reject unsafe threaded-WASI selection, and preserve the official nil-argument behavior.

@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.21622% with 7 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/build/build.go 88.88% 2 Missing and 2 partials ⚠️
internal/build/wasm_postlink.go 95.23% 2 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@cpunion cpunion changed the title runtime/wasm: add opt-in non-moving collector (based on #2192) runtime/wasm: add opt-in non-moving collector (based on #2197) Jul 29, 2026
…orker-asyncify-scheduler

# Conflicts:
#	runtime/internal/runtime/z_default.go
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant